summaryrefslogtreecommitdiff
path: root/app/[lng]/partners/(partners)/techsales/rfq-ship/page.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-05-28 19:03:21 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-05-28 19:03:21 +0000
commit5036cf2908792cef45f06256e71f10920f647f49 (patch)
tree3116e7419e872d45025d1d48e6ddaffe2ba2dd38 /app/[lng]/partners/(partners)/techsales/rfq-ship/page.tsx
parent7ae037e9c2fc0be1fe68cecb461c5e1e837cb0da (diff)
(김준회) 기술영업 조선 RFQ (SHI/벤더)
Diffstat (limited to 'app/[lng]/partners/(partners)/techsales/rfq-ship/page.tsx')
-rw-r--r--app/[lng]/partners/(partners)/techsales/rfq-ship/page.tsx219
1 files changed, 219 insertions, 0 deletions
diff --git a/app/[lng]/partners/(partners)/techsales/rfq-ship/page.tsx b/app/[lng]/partners/(partners)/techsales/rfq-ship/page.tsx
new file mode 100644
index 00000000..5b0ffb61
--- /dev/null
+++ b/app/[lng]/partners/(partners)/techsales/rfq-ship/page.tsx
@@ -0,0 +1,219 @@
+// app/vendor/quotations/page.tsx
+import * as React from "react";
+import Link from "next/link";
+import { Metadata } from "next";
+import { getServerSession } from "next-auth/next";
+import { authOptions } from "@/app/api/auth/[...nextauth]/route";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { LogIn } from "lucide-react";
+import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton";
+import { Shell } from "@/components/shell";
+import { getValidFilters } from "@/lib/data-table";
+import { type SearchParams } from "@/types/table";
+import { searchParamsVendorRfqCache } from "@/lib/techsales-rfq/validations";
+import {
+ TECH_SALES_QUOTATION_STATUSES,
+ TECH_SALES_QUOTATION_STATUS_CONFIG
+} from "@/db/schema";
+
+import { getQuotationStatusCounts, getVendorQuotations } from "@/lib/techsales-rfq/service";
+import { VendorQuotationsTable } from "@/lib/techsales-rfq/vendor-response/table/vendor-quotations-table";
+
+export const metadata: Metadata = {
+ title: "기술영업 견적서 관리",
+ description: "기술영업 RFQ 견적서를 관리합니다.",
+};
+
+interface VendorQuotationsPageProps {
+ searchParams: SearchParams;
+}
+
+export default async function VendorQuotationsPage({
+ searchParams,
+}: VendorQuotationsPageProps) {
+ // 세션 확인
+ const session = await getServerSession(authOptions);
+
+ if (!session?.user) {
+ return (
+ <Shell>
+ <div className="flex min-h-[400px] flex-col items-center justify-center space-y-4">
+ <div className="text-center">
+ <h2 className="text-2xl font-bold tracking-tight">로그인이 필요합니다</h2>
+ <p className="text-muted-foreground">
+ 견적서를 확인하려면 로그인해주세요.
+ </p>
+ </div>
+ <Button asChild>
+ <Link href="/api/auth/signin">
+ <LogIn className="mr-2 h-4 w-4" />
+ 로그인
+ </Link>
+ </Button>
+ </div>
+ </Shell>
+ );
+ }
+
+ // 벤더 ID 확인 (사용자의 회사 ID가 벤더 ID)
+ const vendorId = session.user.companyId;
+ if (!vendorId) {
+ return (
+ <Shell>
+ <div className="flex min-h-[400px] flex-col items-center justify-center space-y-4">
+ <div className="text-center">
+ <h2 className="text-2xl font-bold tracking-tight">회사 정보가 없습니다</h2>
+ <p className="text-muted-foreground">
+ 견적서를 확인하려면 회사 정보가 필요합니다.
+ </p>
+ </div>
+ </div>
+ </Shell>
+ );
+ }
+
+ // 검색 파라미터 파싱 및 검증
+ const search = searchParamsVendorRfqCache.parse(searchParams);
+ const validFilters = getValidFilters(search.filters);
+
+ // 견적서 상태별 개수 조회
+ const statusCountsPromise = getQuotationStatusCounts(vendorId.toString());
+
+ // 견적서 목록 조회
+ const quotationsPromise = getVendorQuotations(
+ {
+ flags: search.flags,
+ page: search.page,
+ perPage: search.perPage,
+ sort: search.sort,
+ filters: validFilters,
+ joinOperator: search.joinOperator,
+ search: search.search,
+ from: search.from,
+ to: search.to,
+ },
+ vendorId.toString()
+ );
+
+ return (
+ <Shell variant="fullscreen" className="h-full">
+ {/* 고정 헤더 영역 */}
+ <div className="flex-shrink-0">
+ <div className="flex-shrink-0 flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
+ <div>
+ <h1 className="text-3xl font-bold tracking-tight">기술영업 견적서</h1>
+ <p className="text-muted-foreground">
+ 할당받은 RFQ에 대한 견적서를 작성하고 관리합니다.
+ </p>
+ </div>
+ </div>
+
+ {/* 상태별 개수 카드 */}
+ <div className="flex-shrink-0">
+ <React.Suspense
+ fallback={
+ <div className="w-full overflow-x-auto">
+ <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 min-w-fit">
+ {Array.from({ length: 5 }).map((_, i) => (
+ <Card key={i} className="min-w-[160px]">
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+ <CardTitle className="text-sm font-medium truncate">로딩중...</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="text-2xl font-bold">-</div>
+ </CardContent>
+ </Card>
+ ))}
+ </div>
+ </div>
+ }
+ >
+ <StatusCards statusCountsPromise={statusCountsPromise} />
+ </React.Suspense>
+ </div>
+
+ {/* 견적서 테이블 */}
+ <div className="flex-1 min-h-0 overflow-hidden">
+ <React.Suspense
+ fallback={
+ <DataTableSkeleton
+ columnCount={12}
+ searchableColumnCount={2}
+ filterableColumnCount={3}
+ cellWidths={["10rem", "15rem", "12rem", "12rem", "8rem"]}
+ shrinkZero
+ />
+ }
+ >
+ <div className="h-full overflow-auto">
+ <VendorQuotationsTable promises={Promise.all([quotationsPromise.then(result => ({ data: result.data, pageCount: result.pageCount }))])} />
+ </div>
+ </React.Suspense>
+ </div>
+ </div>
+ </Shell>
+ );
+}
+
+// 상태별 개수 카드 컴포넌트
+async function StatusCards({
+ statusCountsPromise,
+}: {
+ statusCountsPromise: Promise<{
+ data: { status: string; count: number }[] | null;
+ error: string | null;
+ }>;
+}) {
+ const { data: statusCounts, error } = await statusCountsPromise;
+
+ if (error || !statusCounts) {
+ return (
+ <div className="w-full overflow-x-auto">
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 min-w-fit">
+ <Card className="min-w-[160px]">
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+ <CardTitle className="text-sm font-medium truncate">오류</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="text-2xl font-bold text-red-600">-</div>
+ <p className="text-xs text-muted-foreground truncate">
+ 데이터를 불러올 수 없습니다
+ </p>
+ </CardContent>
+ </Card>
+ </div>
+ </div>
+ );
+ }
+
+ // 중앙화된 상태 설정 사용
+ const statusEntries = Object.entries(TECH_SALES_QUOTATION_STATUSES).map(([, statusValue]) => ({
+ key: statusValue,
+ ...TECH_SALES_QUOTATION_STATUS_CONFIG[statusValue]
+ }));
+
+ console.log(statusCounts, "statusCounts")
+
+ return (
+ <div className="w-full overflow-x-auto">
+ <div className="grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 min-w-fit">
+ {statusEntries.map((status) => (
+ <Card key={status.key} className="min-w-[160px]">
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
+ <CardTitle className="text-sm font-medium truncate">{status.label}</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className={`text-2xl font-bold ${status.color}`}>
+ {statusCounts.find(item => item.status === status.key)?.count || 0}
+ </div>
+ <p className="text-xs text-muted-foreground truncate">
+ {status.description}
+ </p>
+ </CardContent>
+ </Card>
+ ))}
+ </div>
+ </div>
+ );
+} \ No newline at end of file